egw_data.js ➔ parseServerResponse   F
last analyzed

Complexity

Conditions 24

Size

Total Lines 134
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 79
dl 0
loc 134
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like egw_data.js ➔ parseServerResponse often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * eGroupWare eTemplate2
3
 *
4
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5
 * @package etemplate
6
 * @subpackage api
7
 * @link http://www.egroupware.org
8
 * @author Andreas Stöckel
9
 * @copyright Stylite 2012
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
	egw_core;
15
	egw_debug;
16
*/
17
18
/**
19
 * Module storing and updating row data
20
 *
21
 * @param {string} _app application name object is instanciated for
22
 * @param {object} _wnd window object is instanciated for
23
 */
24
egw.extend("data", egw.MODULE_APP_LOCAL, function (_app, _wnd)
25
{
26
	"use strict";
27
28
	/**
29
	 * How many UIDs we'll tell the server we know about.  No need to pass the whole list around.
30
	 */
31
	var KNOWN_UID_LIMIT = 200;
32
33
	/**
34
	 * Cache lifetime
35
	 *
36
	 * If cached results are used, we check their timestamp.  If the timestamp
37
	 * is older than this, we will also ask for fresh data.  For cached data
38
	 * younger than this, we only return the cache
39
	 *
40
	 * 29 seconds, 1 less then the fastest nextmatch autorefresh option
41
	 */
42
	var CACHE_LIFETIME = 29; // seconds
43
44
	/**
45
	 * Cached fetches are differentiated from actual results by using this prefix
46
	 * @type String
47
	 */
48
	var CACHE_KEY_PREFIX = 'cached_fetch_';
49
50
	var lastModification = null;
51
52
	/**
53
	 * cacheCallback stores callbacks that determine if data is placed
54
	 * into cacheStorage, or simply kept temporarily.  It is indexed
55
	 * by prefix.
56
	 *
57
	 * @type Array
58
	 */
59
	var cacheCallback = {};
60
61
	/**
62
	 * The uid function generates a session-unique id for the current
63
	 * application by appending the application name to the given uid.
64
	 *
65
	 * @param {string} _uid
66
	 * @param {string} _prefix
67
	 */
68
	function UID(_uid, _prefix)
69
	{
70
		_prefix = _prefix ? _prefix : _app;
71
72
		return _prefix + "::" + _uid;
73
	}
74
75
	/**
76
	 * Looks like too much data is cached.  Forget some.
77
	 *
78
	 * Tries to free up localStorage by removing the oldest cached data for the
79
	 * given prefix, but if none is found it will look at all cached data.
80
	 *
81
	 * @param {string} _prefix UID / application prefix
82
	 * @returns {Number} Number of cached recordsets removed, normally 1.
83
	 */
84
	function _clearCache(_prefix)
85
	{
86
		// Find cached items for the prefix, we prefer to expire just within the app
87
		var indexes = [];
88
		for(var i = 0; i < window.localStorage.length; i++)
89
		{
90
			var key = window.localStorage.key(i);
91
92
			// This is a cached fetch for many rows
93
			if(key.indexOf(CACHE_KEY_PREFIX+_prefix) == 0)
94
			{
95
				var cached = JSON.parse(window.localStorage.getItem(key));
96
97
				if(cached.lastModification)
98
				{
99
					indexes.push({
100
						key: key,
101
						lastModification: cached.lastModification
102
					});
103
				}
104
				else
105
				{
106
					// No way to know how old it is, just remove it
107
					window.localStorage.removeItem(key);
108
				}
109
			}
110
			// Actual cached data
111
			else if (key.indexOf(_prefix) == 0)
112
			{
113
				var cached = JSON.parse(window.localStorage.getItem(key));
114
				if(cached.timestamp)
115
				{
116
					indexes.push({
117
						key: key,
118
						lastModification: cached.timestamp
119
					});
120
				}
121
				else
122
				{
123
					// No way to know how old it is, just remove it
124
					window.localStorage.removeItem(key);
125
				}
126
			}
127
		}
128
		// Nothing for that prefix?  Clear all cached data.
129
		if(_prefix && indexes.length == 0)
130
		{
131
			return _clearCache('');
132
		}
133
		// Found some cached for that prefix, only remove the oldest
134
		else if (indexes.length > 0)
135
		{
136
			indexes.sort(function(a,b) {
137
				if(a.lastModification < b.lastModification) return 1;
138
				if(a.lastModification > b.lastModification) return -1;
139
				return 0;
140
			});
141
			window.localStorage.removeItem(indexes.pop().key);
142
			return 1;
143
		}
144
		return indexes.length;
145
	}
146
147
	function parseServerResponse(_result, _callback, _context, _execId, _widgetId)
148
	{
149
		// Check whether the result is valid
150
		// This result is not for us, quietly return
151
		if(_result && typeof _result.type != 'undefined') return;
152
153
		// "result" has to be an object consting of "order" and "data"
154
		if (!(_result && typeof _result.order !== "undefined"
155
		    && typeof _result.data !== "undefined"))
156
		{
157
			egw.debug("error", "Invalid result for 'dataFetch'");
158
		}
159
160
		if (_result.lastModification)
161
		{
162
			lastModification = _result.lastModification;
163
		}
164
165
		if (_result.order && _result.data)
166
		{
167
			// Assemble the correct order uids
168
			if(!(_result.order.length && _result.order[0] && _result.order[0].indexOf && _result.order[0].indexOf(_context.prefix) == 0))
169
			{
170
				for (var i = 0; i < _result.order.length; i++)
171
				{
172
					_result.order[i] = UID(_result.order[i], _context.prefix);
173
				}
174
			}
175
176
			// Load all data entries that have been sent or delete them
177
			for (var key in _result.data)
178
			{
179
				var uid = UID(key, (typeof _context == "object" && _context != null) ?_context.prefix : "");
180
				if (_result.data[key] === null &&
181
				(
182
					typeof _context.refresh == "undefined" || _context.refresh && !jQuery.inArray(key,_context.refresh)
183
				))
184
				{
185
					egw.dataDeleteUID(uid);
186
				}
187
				else
188
				{
189
					egw.dataStoreUID(uid, _result.data[key]);
190
				}
191
			}
192
193
			// Tried to refresh a specific row and got nothing, so set it to null
194
			// (triggers update for listeners), then remove it
195
			if(_result.order.length == 0 && typeof _context == "object" && _context.refresh)
196
			{
197
				for(var i = 0; i < _context.refresh.length; i++)
198
				{
199
					var uid = UID(_context.refresh[i], _context.prefix);
200
					egw.dataStoreUID(uid, null);
201
					egw.dataDeleteUID(uid);
202
				}
203
			}
204
205
			// Check to see if we need long-term caching of the query and its results
206
			if(window.localStorage && _context.prefix && cacheCallback[_context.prefix]  && !_context.no_cache)
207
			{
208
				// Ask registered callbacks if we should cache this
209
				for(var i = 0; i < cacheCallback[_context.prefix].length; i++)
210
				{
211
					var cc = cacheCallback[_context.prefix][i];
212
					var cache_key = cc.callback.call(cc.context, _context);
213
					if(cache_key)
214
					{
215
						cache_key = CACHE_KEY_PREFIX + _context.prefix + '::' + cache_key;
216
						try
217
						{
218
							for (var key in _result.data)
219
							{
220
								var uid = UID(key, (typeof _context == "object" && _context != null) ? _context.prefix : "");
221
222
								// Register a handler on each data so we can know if it is updated or removed
223
								egw.dataUnregisterUID(uid, null, cache_key);
224
								egw.dataRegisterUID(uid, function(data, _uid) {
225
									// If data item is removed, remove it from cached fetch too
226
									if(data == null)
227
									{
228
										var cached = JSON.parse(window.localStorage[this]) || false;
229
										if(cached && cached.order && cached.order.indexOf(_uid) >= 0)
230
										{
231
											cached.order.splice(cached.order.indexOf(_uid),1);
232
											if(cached.total) cached.total--;
233
											window.localStorage[this] = JSON.stringify(cached);
234
										}
235
										window.localStorage.removeItem(_uid);
236
									}
237
									else
238
									{
239
										// Update or store data in long-term storage
240
										window.localStorage[_uid] = JSON.stringify({timestamp: (new Date).getTime(), data: data});
241
									}
242
								}, cache_key, _execId, _widgetId);
243
							}
244
							// Don't keep data in long-term cache with request also
245
							_result.data = {};
246
							window.localStorage.setItem(cache_key,JSON.stringify(_result));
247
						}
248
						catch (e)
249
						{
250
							// Maybe ran out of space?  Free some up.
251
							if(e.name == 'QuotaExceededError'	// storage quota is exceeded, remove cached data
252
								|| e.name == 'NS_ERROR_DOM_QUOTA_REACHED')	// FF-name
253
							{
254
								var count = _clearCache(_context.prefix);
255
								egw.debug('info', 'localStorage full, removed ' + count + ' stored datasets');
256
							}
257
							// No, something worse happened
258
							else
259
							{
260
								egw.debug('warning', 'Tried to cache some data.  It did not work.', cache_key, e);
261
							}
262
						}
263
					}
264
				}
265
			}
266
267
			// Call the callback function and pass the calculated "order" array
268
			// as well as the "total" count and the "timestamp" to the listener.
269
			if (_callback)
270
			{
271
				_callback.call(_context, {
272
					"order": _result.order,
273
					"total": parseInt(_result.total),
274
					"readonlys": _result.readonlys,
275
					"rows": _result.rows,
276
					"lastModification": lastModification
277
				});
278
			}
279
		}
280
	}
281
282
	return {
283
284
		/**
285
		 * The dataFetch function provides an abstraction layer for the
286
		 * corresponding "EGroupware\Api\Etemplate\Widget\Nextmatch::ajax_get_rows" function.
287
		 * The server returns the following structure:
288
		 * 	{
289
		 * 		order: [uid, ...],
290
		 * 		data:
291
		 * 			{
292
		 * 				uid0: data,
293
		 * 				...
294
		 * 				uidN: data
295
		 * 			},
296
		 * 		total: <TOTAL COUNT>,
297
		 * 		lastModification: <LAST MODIFICATION TIMESTAMP>,
298
		 * 		readonlys: <READONLYS>
299
		 * 	}
300
		 * If a uid got deleted on the server above data is null.
301
		 * If a uid is omitted from data, is has not changed since lastModification.
302
		 *
303
		 * If order/data is null, this means that nothing has changed for the
304
		 * given range.
305
		 * The dataFetch function stores new data for the uid's inside the
306
		 * local data storage, the grid views are then capable of querying the
307
		 * data for those uids from the local storage using the
308
		 * "dataRegisterUID" function.
309
		 *
310
		 * @param _execId is the execution context of the etemplate instance
311
		 * 	you're querying the data for.
312
		 * @param _queriedRange is an object of the following form:
313
		 * 	{
314
		 * 		start: <START INDEX>,
315
		 * 		num_rows: <COUNT OF ENTRIES>
316
		 * 	}
317
		 * The range always corresponds to the given filter settings.
318
		 * @param _filters contains the filter settings. The filter settings are
319
		 * 	those which are crucial for the mapping between index and uid.
320
		 * @param _widgetId id with full namespace of widget
321
		 * @param _callback is the function that should get called, once the data
322
		 * 	is available. The data passed to the callback function has the
323
		 * 	following form:
324
		 * 	{
325
		 * 		order: [uid, ...],
326
		 * 		total: <TOTAL COUNT>,
327
		 * 		lastModification: <LAST MODIFICATION TIMESTAMP>,
328
		 * 		readonlys: <READONLYS>
329
		 * 	}
330
		 * 	Please note that the "uids" comming from the server and the ones
331
		 * 	being parsed to the callback function differ. While the uids
332
		 * 	which are returned from the server are only unique inside the
333
		 * 	application, the uids which are used on the client are "globally"
334
		 * 	unique.
335
		 * @param _context is the context in which the callback function will get
336
		 * 	called.
337
		 * @param _knownUids is an array of uids already known to the client.
338
		 *  This parameter may be null in order to indicate that the client
339
		 *  currently has no data for the given filter settings.
340
		 */
341
		dataFetch: function (_execId, _queriedRange, _filters, _widgetId,
342
				_callback, _context, _knownUids)
343
		{
344
			var lm = lastModification;
345
			if(typeof _context.lastModification != "undefined") lm = _context.lastModification;
346
347
			if (_queriedRange["no_data"])
348
			{
349
				lm = 0xFFFFFFFFFFFF;
350
			}
351
			else if (_queriedRange["only_data"])
352
			{
353
				lm = 0;
354
			}
355
356
			// Store refresh in context to not delete the other entries when server only returns these
357
			if (typeof _queriedRange.refresh != "undefined")
358
			{
359
				if(typeof _queriedRange.refresh == "string")
360
				{
361
					_context.refresh = [_queriedRange.refresh];
362
				}
363
				else
364
				{
365
					_context.refresh = _queriedRange.refresh;
366
				}
367
			}
368
369
			// Limit the amount of UIDs we say we know about to a sensible number, in case user is enjoying auto-pagination
370
			var knownUids = _knownUids ? _knownUids : egw.dataKnownUIDs(_context.prefix ? _context.prefix : _app);
371
			if(knownUids > KNOWN_UID_LIMIT)
372
			{
373
				knownUids.slice(typeof _queriedRange.start != "undefined" ? _queriedRange.start:0,KNOWN_UID_LIMIT);
374
			}
375
376
			// Check to see if we have long-term caching of the query and its results
377
			if(window.localStorage && _context.prefix && cacheCallback[_context.prefix])
378
			{
379
				// Ask registered callbacks if we should cache this
380
				for(var i = 0; i < cacheCallback[_context.prefix].length; i++)
381
				{
382
					var cc = cacheCallback[_context.prefix][i];
383
					var cache_key = cc.callback.call(cc.context, _context);
384
					if(cache_key)
385
					{
386
						cache_key = CACHE_KEY_PREFIX + _context.prefix + '::' + cache_key;
387
388
						var cached = window.localStorage.getItem(cache_key);
389
						if(cached)
390
						{
391
							cached = JSON.parse(cached);
392
							var needs_update = true;
393
394
							// Check timestamp
395
							if(cached.lastModification && ((Date.now()/1000) - cached.lastModification) < CACHE_LIFETIME)
396
							{
397
								needs_update = false;
398
							}
399
400
							egw.debug('log', 'Data cached query from ' + new Date(cached.lastModification*1000)+': ' + cache_key + '('+
401
								(needs_update ? 'will be' : 'will not be')+" updated)\nprocessing...");
402
403
							// Call right away with cached data, but set no_cache flag
404
							// to avoid re-caching this data with a new timestamp.
405
							// We may still ask the server though.
406
							var no_cache = _context.no_cache;
407
							_context.no_cache = true;
408
							parseServerResponse(cached, _callback, _context, _execId, _widgetId);
409
							_context.no_cache = no_cache;
410
411
412
							// If cache registrant wants notification of cache useage,
413
							// let it know
414
							if(cc.notification)
415
							{
416
								cc.notification.call(cc.context, needs_update);
417
							}
418
419
							if(!needs_update)
420
							{
421
								// Cached data is new enough, skip the server call
422
								return;
423
							}
424
						}
425
					}
426
				}
427
			}
428
			// create a clone of filters, which can be used in parseServerResponse and cache callbacks
429
			// independent of changes happening while waiting for the response
430
			_context.filters = jQuery.extend({}, _filters);
431
			var request = egw.json(
432
				"EGroupware\\Api\\Etemplate\\Widget\\Nextmatch::ajax_get_rows",
433
				[
434
					_execId,
435
					_queriedRange,
436
					_filters,
437
					_widgetId,
438
					knownUids,
439
					lm
440
				],
441
				function(result) {
442
					parseServerResponse(result, _callback, _context, _execId, _widgetId);
443
				},
444
				this,
445
				true
446
			);
447
			request.sendRequest();
448
		},
449
450
		/**
451
		 * Turn on long-term client side cache of a particular request
452
		 * (cache the nextmatch query results) for fast, immediate response
453
		 * with old data.
454
		 *
455
		 * The request is still sent to the server, and the cache is updated
456
		 * with fresh data, and any needed callbacks are called again with
457
		 * the fresh data.
458
		 *
459
		 * @param {string} prefix UID / Application prefix should match the
460
		 *	individual record prefix
461
		 * @param {function} callback_function A function that will analize the provided fetch
462
		 *	parameters and return a reproducable cache key, or false to not cache
463
		 *	the request.
464
		 * @param {function} notice_function A function that will be called whenever
465
		 *	cached data is used.  It is passed one parameter, a boolean that indicates
466
		 *	if the server is or will be queried to refresh the cache.  Do not fetch additional data
467
		 *	inside this callback, and return quickly.
468
		 * @param {object} context Context for callback function.
469
		 */
470
		dataCacheRegister: function(prefix, callback_function, notice_function, context)
471
		{
472
			if(typeof cacheCallback[prefix] == 'undefined')
473
			{
474
				cacheCallback[prefix] = [];
475
			}
476
			cacheCallback[prefix].push({
477
				callback: callback_function,
478
				notification: notice_function || false,
479
				context: context || null
480
			});
481
		},
482
483
		/**
484
		 * Unregister a previously registered cache callback
485
		 * @param {string} prefix UID / Application prefix should match the
486
		 *	individual record prefix
487
		 * @param {function} [callback] Callback function to un-register.  If
488
		 *	omitted, all functions for the prefix will be removed.
489
		 */
490
		dataCacheUnregister: function(prefix, callback)
491
		{
492
			if(typeof callback != 'undefined')
493
			{
494
				for(var i = 0; i < cacheCallback[prefix].length; i++)
495
				{
496
					if(cacheCallback[prefix][i].callback == callback)
497
					{
498
						cacheCallback[prefix].splice(i,1);
499
						return;
500
					}
501
				}
502
			}
503
			// Callback not provided or not found, reset by prefix
504
			cacheCallback[prefix] = [];
505
		}
506
	};
507
508
});
509
510
egw.extend("data_storage", egw.MODULE_GLOBAL, function (_app, _wnd) {
511
512
	/**
513
	 * The localStorage object is used to store the data for certain uids. An
514
	 * entry inside the localStorage object looks like the following:
515
	 * 	{
516
	 * 		timestamp: <CREATION TIMESTAMP (local)>,
517
	 * 		data: <DATA>
518
	 * 	}
519
	 */
520
	var localStorage = {};
521
522
	/**
523
	 * The registeredCallbacks map is used to store all callbacks registerd for
524
	 * a certain uid.
525
	 */
526
	var registeredCallbacks = {};
527
528
529
530
	/**
531
	 * Register the "data" plugin globally for single uids
532
	 * Multiple UIDs such as nextmatch results are still handled by egw.data
533
	 * using dataFetch() && parseServerResponse(), above.  Both update the
534
	 * GLOBAL data cache though this one is registered globally, and the above
535
	 * is registered app local.
536
	 *
537
	 * @param {string} type
538
	 * @param {object} res
539
	 * @param {object} req
540
	 * @returns {Boolean}
541
	 */
542
	egw.registerJSONPlugin(function(type, res, req) {
543
		if ((typeof res.data.uid != 'undefined') &&
544
			(typeof res.data.data != 'undefined'))
545
		{
546
			// Store it, which will call all registered listeners
547
			this.dataStoreUID(res.data.uid, res.data.data);
548
			return true;
549
		}
550
	}, egw, 'data',true);
551
552
	/**
553
	 * Uids and timers used for querying data uids, hashed by the first few
554
	 * bytes of the _execId, stored as an object of the form
555
	 * {
556
	 *     "timer": <QUEUE TIMER>,
557
	 *     "uids": <ARRAY OF UIDS>
558
	 * }
559
	 */
560
	var queue = {};
561
562
	/**
563
	 * Contains the queue timeout in milliseconds.
564
	 */
565
	var QUEUE_TIMEOUT = 10;
566
567
	/**
568
	 * This constant specifies the maximum age of entries in the local storrage
569
	 * in milliseconds
570
	 */
571
	var MAX_AGE = 5 * 60 * 1000; // 5 mins
572
573
	/**
574
	 * This constant specifies the interval in which the local storage gets
575
	 * cleaned up.
576
	 */
577
	var CLEANUP_INTERVAL = 30 * 1000; // 30 sec
578
579
	/**
580
	 * Register a cleanup function, which throws away all data entries which are
581
	 * older than the given age.
582
	 */
583
	_wnd.setInterval(function() {
584
		// Get the current timestamp
585
		var time = (new Date).getTime();
586
587
		// Iterate over the local storage
588
		for (var uid in localStorage)
589
		{
590
			// Expire old data, if there are no callbacks
591
			if (time - localStorage[uid].timestamp > MAX_AGE && typeof registeredCallbacks[uid] == "undefined")
592
			{
593
				// Unregister all registered callbacks for that uid
594
				egw.dataUnregisterUID(uid);
595
596
				// Delete the data from the localStorage
597
				delete localStorage[uid];
598
599
				// We don't clean long-term storage because of age until it runs
600
				// out of space
601
			}
602
		}
603
	}, CLEANUP_INTERVAL);
604
605
	return {
606
607
		/**
608
		 * Registers the intrest in a certain uid for a callback function. If
609
		 * the data for that uid changes or gets loaded, the given callback
610
		 * function is called. If the data for the given uid is available at the
611
		 * time of registering the callback, the callback is called immediately.
612
		 *
613
		 * @param _uid is the uid for which the callback should be registered.
614
		 * @param _callback is the callback which should get called.
615
		 * @param _context is the optional context in which the callback will be
616
		 * executed
617
		 * @param _execId is the exec id which will be used in case the data is
618
		 * not available
619
		 * @param _widgetId is the widget id which will be used in case the uid
620
		 * has to be fetched.
621
		 */
622
		dataRegisterUID: function (_uid, _callback, _context, _execId, _widgetId) {
623
			// Create the slot for the uid if it does not exist now
624
			if (typeof registeredCallbacks[_uid] === "undefined")
625
			{
626
				registeredCallbacks[_uid] = [];
627
			}
628
629
			// Store the given callback
630
			registeredCallbacks[_uid].push({
631
				"callback": _callback,
632
				"context": _context ? _context : null,
633
				"execId": _execId,
634
				"widgetId" : _widgetId
635
			});
636
637
			// Check whether the data is available -- if yes, immediately call
638
			// back the callback function
639
			if (typeof localStorage[_uid] !== "undefined")
640
			{
641
				// Update the timestamp and call the given callback function
642
				localStorage[_uid].timestamp = (new Date).getTime();
643
				_callback.call(_context, localStorage[_uid].data, _uid);
644
			}
645
			// Check long-term storage
646
			else if(window.localStorage && window.localStorage[_uid])
647
			{
648
				localStorage[_uid] = JSON.parse(window.localStorage[_uid]);
649
				_callback.call(_context, localStorage[_uid].data, _uid);
650
			}
651
			else if (_execId && _widgetId)
652
			{
653
				// Get the first 50 bytes of the exex id
654
				var hash = _execId.substring(0, 50);
655
656
				// Create a new queue if it does not exist yet
657
				if (typeof queue[hash] === "undefined")
658
				{
659
					var self = this;
660
					queue[hash] = { "uids": [], "timer": null };
661
					queue[hash].timer = window.setTimeout(function () {
662
						// Fetch the data
663
						self.dataFetch(_execId, {"start": 0, "num_rows": 0, "only_data": true, "refresh": queue[hash].uids},
664
							[], _widgetId, null, _context, null);
665
666
						// Delete the queue entry
667
						delete queue[hash];
668
					}, 100);
669
				}
670
671
				// Push the uid onto the queue, removing the prefix
672
				var parts = _uid.split("::");
673
				parts.shift();
674
				queue[hash].uids.push(parts.join('::'));
675
			}
676
			else
677
			{
678
				this.debug("log", "Data for uid " + _uid + " not available.");
679
			}
680
		},
681
682
		/**
683
		 * Unregisters the intrest of updates for a certain data uid.
684
		 *
685
		 * @param _uid is the data uid for which the callbacks should be
686
		 * 	unregistered.
687
		 * @param _callback specifies the specific callback that should be
688
		 * 	unregistered. If it evaluates to false, all callbacks (or those
689
		 * 	matching the optionally given context) are removed.
690
		 * @param _context specifies the callback context that should be
691
		 * 	unregistered. If it evaluates to false, all callbacks (or those
692
		 * 	matching the optionally given callback function) are removed.
693
		 */
694
		dataUnregisterUID: function (_uid, _callback, _context) {
695
696
			// Force the optional parameters to be exactly null
697
			_callback = _callback ? _callback : null;
698
			_context = _context ? _context : null;
699
700
			if (typeof registeredCallbacks[_uid] !== "undefined")
701
			{
702
703
				// Iterate over the registered callbacks for that uid and delete
704
				// all callbacks pointing to the given callback and context
705
				for (var i = registeredCallbacks[_uid].length - 1; i >= 0; i--)
706
				{
707
					if ((!_callback || registeredCallbacks[_uid][i].callback === _callback)
708
					    && (!_context || registeredCallbacks[_uid][i].context === _context))
709
					{
710
						registeredCallbacks[_uid].splice(i, 1);
711
					}
712
				}
713
714
				// Delete the slot if no callback is left for the uid
715
				if (registeredCallbacks[_uid].length === 0)
716
				{
717
					delete registeredCallbacks[_uid];
718
				}
719
			}
720
		},
721
722
		/**
723
		 * Returns whether data is available for the given uid.
724
		 *
725
		 * @param _uid is the uid for which should be checked whether it has some
726
		 * 	data.
727
		 */
728
		dataHasUID: function (_uid) {
729
			return typeof localStorage[_uid] !== "undefined";
730
		},
731
732
		/**
733
		 * Returns data of a given uid.
734
		 *
735
		 * @param _uid is the uid for which should be checked whether it has some
736
		 * 	data.
737
		 */
738
		dataGetUIDdata: function (_uid) {
739
			return localStorage[_uid];
740
		},
741
742
		/**
743
		 * Returns all uids that have the given prefix
744
		 *
745
		 * @param {string} _prefix
746
		 * @return {array}
747
		 * TODO: Improve this
748
		 */
749
		dataKnownUIDs: function (_prefix) {
750
751
			var result = [];
752
753
			for (var key in localStorage)
754
			{
755
				var parts = key.split("::");
756
				if (parts.shift() === _prefix && localStorage[key].data)
757
				{
758
759
					result.push(parts.join('::'));
760
				}
761
			}
762
763
			return result;
764
		},
765
766
		/**
767
		 * Stores data for the uid and calls all callback functions registered
768
		 * for that uid.
769
		 *
770
		 * @param _uid is the uid for which the data should be saved.
771
		 * @param _data is the data which should be saved.
772
		 */
773
		dataStoreUID: function (_uid, _data) {
774
			// Get the current unix timestamp
775
			var timestamp = (new Date).getTime();
776
777
			// Store the data in the local storage
778
			localStorage[_uid] = {
779
				"timestamp": timestamp,
780
				"data": _data
781
			};
782
783
			// Inform all registered callback functions and pass the data to
784
			// those.
785
			if (typeof registeredCallbacks[_uid] != "undefined")
786
			{
787
				for (var i = registeredCallbacks[_uid].length - 1; i >= 0; i--)
788
				{
789
					try {
790
						registeredCallbacks[_uid][i].callback.call(
791
							registeredCallbacks[_uid][i].context,
792
							_data,
793
							_uid
794
						);
795
					} catch (e) {
796
						// Remove this callback from the list
797
						registeredCallbacks[_uid].splice(i, 1);
798
					}
799
				}
800
			}
801
		},
802
803
		/**
804
		 * Deletes the data for a certain uid from the local storage and
805
		 * unregisters all callback functions associated to it.
806
		 *
807
		 * This does NOT update nextmatch!
808
		 * Application code should use: egw(window).refresh(msg, app, id, "delete");
809
		 *
810
		 * @param _uid is the uid which should be deleted.
811
		 */
812
		dataDeleteUID: function (_uid) {
813
			if (typeof localStorage[_uid] !== "undefined")
814
			{
815
				// Delete the element from the local storage
816
				delete localStorage[_uid];
817
818
				// Unregister all callbacks for that uid
819
				this.dataUnregisterUID(_uid);
820
			}
821
		},
822
823
		/**
824
		 * Force a refreash of the given uid from the server if known, and
825
		 * calls all associated callbacks.
826
		 *
827
		 * If the UID does not have any registered callbacks, it cannot be refreshed because the required
828
		 * execID and context are missing.
829
		 *
830
		 * @param {string} _uid is the uid which should be refreshed.
831
		 * @return {boolean} True if the uid is known and can be refreshed, false if unknown and will not be refreshed
832
		 */
833
		dataRefreshUID: function (_uid) {
834
			if (typeof localStorage[_uid] === "undefined") return false;
835
836
			if(typeof registeredCallbacks[_uid] !== "undefined" && registeredCallbacks[_uid].length > 0)
837
			{
838
				var _execId = registeredCallbacks[_uid][0].execId;
839
				// This widget ID MUST be a nextmatch, because the data call is to Etemplate\Widget\Nexmatch
840
				var nextmatchId = registeredCallbacks[_uid][0].widgetId;
841
				var uid = _uid.split("::");
842
				var context = {
843
					"prefix":uid.shift()
844
				};
845
				uid = uid.join("::");
846
847
				// find filters, even if context is not always from nextmatch, eg. caching uses it's a string context
848
				var filters = {};
849
				for(var i=0; i < registeredCallbacks[_uid].length; i++)
850
				{
851
					var callback = registeredCallbacks[_uid][i];
852
					if (typeof callback.context == 'object' &&
853
						typeof callback.context.self == 'object' &&
854
						typeof callback.context.self._filters == 'object')
855
					{
856
						filters = callback.context.self._filters;
857
						break;
858
					}
859
				}
860
861
				// need to send nextmatch filters too, as server-side will merge old version from request otherwise
862
				this.dataFetch(_execId, {'refresh':uid}, filters, nextmatchId, false, context, [uid]);
863
864
				return true;
865
			}
866
			return false;
867
		},
868
869
		/**
870
		 * Search for exact UID string or regular expression and return widgets using it
871
		 *
872
		 * @param {string|RegExp} _uid is the uid which should be refreshed.
873
		 * @return {object} UID: array of (nextmatch-)wigetIds
874
		 */
875
		dataSearchUIDs: function(_uid)
876
		{
877
			var matches = {};
878
			var f = function(_uid)
879
			{
880
				if (typeof matches[_uid] == "undefined")
881
				{
882
					matches[_uid] = [];
883
				}
884
				if (typeof registeredCallbacks[_uid] !== "undefined")
885
				{
886
					for(var n=0; n < registeredCallbacks[_uid].length; ++n)
887
					{
888
						var callback = registeredCallbacks[_uid][n];
889
						if (typeof callback.context != "undefined" &&
890
							typeof callback.context.self != "undefined" &&
891
							typeof callback.context.self._widget != "undefined")
892
						{
893
							matches[_uid].push(callback.context.self._widget);
894
						}
895
					}
896
				}
897
			};
898
			if (typeof _uid == "object" && _uid.constructor.name == "RegExp")
899
			{
900
				for(var uid in localStorage)
901
				{
902
					if (_uid.test(uid))
903
					{
904
						f(uid);
905
					}
906
				}
907
			}
908
			else if (typeof localStorage[_uid] != "undefined")
909
			{
910
				f(_uid);
911
			}
912
			return matches;
913
		},
914
915
		/**
916
		 * Search for exact UID string or regular expression and call registered (nextmatch-)widgets refresh function with given _type
917
		 *
918
		 * This method is preferable over dataRefreshUID for app code, as it takes care of things like counters too.
919
		 *
920
		 * It does not do anything for _type="add"!
921
		 *
922
		 * @param {string|RegExp) _uid is the uid which should be refreshed.
923
		 * @param {string} _type "delete", "edit", "update", not useful for "add"!
924
		 * @return {array} (nextmatch-)wigets refreshed
925
		 */
926
		dataRefreshUIDs: function(_uid, _type)
927
		{
928
			var uids = this.dataSearchUIDs(_uid);
929
			var widgets = [];
930
			var uids4widget = [];
931
			for(var uid in uids)
932
			{
933
				for(var n=0; n < uids[uid].length; ++n)
934
				{
935
					var widget = uids[uid][n];
936
					var idx = widgets.indexOf(widget);
937
					if (idx == -1)
938
					{
939
						widgets.push(widget);
940
						idx = widgets.length-1;
941
					}
942
					// uids for nextmatch.refesh do NOT contain the prefix
943
					var nm_uid = uid.replace(RegExp('^'+widget.controller.dataStorePrefix+'::'), '');
944
					if (typeof uids4widget[idx] == "undefined")
945
					{
946
						uids4widget[idx] = [nm_uid];
947
					}
948
					else
949
					{
950
						uids4widget[idx].push(nm_uid);
951
					}
952
				}
953
			}
954
			for(var w=0; w < widgets.length; ++w)
955
			{
956
				widgets[w].refresh(uids4widget[w], _type);
957
			}
958
			return widgets;
959
		}
960
	};
961
});
962